package org.romzzz.gwtclassifieds.server.service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import org.datanucleus.exceptions.NucleusObjectNotFoundException;
import org.romzzz.gwtclassifieds.client.data.CategoryBean;
import org.romzzz.gwtclassifieds.client.data.ClassifiedBean;
import org.romzzz.gwtclassifieds.client.exception.AuthenticationException;
import org.romzzz.gwtclassifieds.server.data.Category;
import org.romzzz.gwtclassifieds.server.data.Classified;
import org.romzzz.gwtclassifieds.server.data.FeedbackMessage;
import com.google.appengine.api.users.UserServiceFactory;

/**
 * This class stores and retrieves all the required data using JDO and DataNucleus.
 * It also uses Google AppEngine API to check the current user's status.
 */
public class ClassifiedsStorage {

	private static ClassifiedsStorage instance = null;
	
	private PersistenceManagerFactory pmf;
	private Map<Long, Long> tokens;
	
	/**
	 * Get an instance of the class.
	 * @return instance
	 */
	public static ClassifiedsStorage getInstance() {
		if (instance == null) {
			instance = new ClassifiedsStorage();
		}
		return instance;
	}
	
	private ClassifiedsStorage() {
		pmf = JDOHelper.getPersistenceManagerFactory("transactions-optional");
		tokens = new HashMap<Long, Long>();
	}
	
	private PersistenceManagerFactory getPersistenceManagerFactory() {
		return pmf;
	}
	
	/**
	 * Add a category to the database. The existence check of the category is made client-side.
	 * 
	 * @param categoryBean the category to add
	 */
	public void addCategory(CategoryBean categoryBean) {
		PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
		pm.currentTransaction().begin();
		try {
			pm.makePersistent(new Category(categoryBean.getValue()));
			pm.currentTransaction().commit();
		} catch (Exception e) {
			System.err.println("Error on creating a category");
			e.printStackTrace(System.err);
			pm.currentTransaction().rollback();
		} finally {
			pm.close();
		}
	}
	
	/**
	 * Remove the given category from the database.
	 * 
	 * @param categoryBean the category to remove
	 */
	public void removeCategory(CategoryBean categoryBean) {
		PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
		pm.currentTransaction().begin();
		try {
			Category category = pm.getObjectById(Category.class, categoryBean.getValue());
			if (category != null) {
				pm.deletePersistent(category);
				pm.currentTransaction().commit();
			}
		} catch (NucleusObjectNotFoundException e) {
			// object does not exist, should not happen
			pm.currentTransaction().rollback();
			System.err.println("Error on removing a category");
			e.printStackTrace(System.err);
		} finally {
			pm.close();
		}
	}
	
	/**
	 * Retrieve all the categories in the database.
	 * 
	 * @return a list of gwt beans
	 */
	@SuppressWarnings("unchecked")
	public List<CategoryBean> getCategories() {
		PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
		String query = "select from " + Category.class.getName();
		List<Category> categories = (List<Category>) pm.newQuery(query).execute();
		List<CategoryBean> categoryBeans = new ArrayList<CategoryBean>(categories.size());
		for (Category cat: categories) {
			categoryBeans.add(new CategoryBean(cat.getValue()));
		}
		Collections.sort(categoryBeans);
		return categoryBeans;
	}
	
	/**
	 * Add a classified to the database. No existence check is made, duplicates are allowed.
	 * 
	 * @param classifiedBean the classified to add (gwt bean)
	 * @param password the password to use
	 */
	public void addClassified(ClassifiedBean classifiedBean, String password) {
		Long id = Long.valueOf(System.currentTimeMillis());
		PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
		pm.currentTransaction().begin();
		pm.makePersistent(new Classified(id, classifiedBean, Encrypter.encrypt(password)));
		pm.currentTransaction().commit();
		pm.close();
	}
	
	/**
	 * Check if a given password grants access to the associate classified. If the current user is an administrator, this check is always bypassed.
	 * 
	 * @param id the classified id
	 * @param password the password to check
	 * @return a token to unlock the associated classified (can be used only once)
	 * @throws AuthenticationException if the password does not match
	 */
	public Long checkAccess(Long id, String password) throws AuthenticationException {
		Long token = null;
		PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
		pm.currentTransaction().begin();
		try {
			Classified classified = pm.getObjectById(Classified.class, id);
			pm.currentTransaction().rollback();
			if (classified != null) {
				String storedPassword = classified.getPassword();
				if (UserServiceFactory.getUserService().isUserAdmin() || (storedPassword != null && storedPassword.equals(Encrypter.encrypt(password)))) {
					token = Long.valueOf(new Random().nextLong());
					tokens.put(id, token);
				}
			}
		} catch (NucleusObjectNotFoundException e) {
			// object does not exist, should not happen
			pm.currentTransaction().rollback();
			System.err.println("This should not happen");
			e.printStackTrace(System.err);
		} finally {
			pm.close();
		}
		if (token != null) {
			return token ;
		} else {
			throw new AuthenticationException("Wrong password");
		}
	}
	
	/**
	 * Update a given classified using the token provided by the {@link #checkAccess(Long, String) checkAccess} method. 
	 * 
	 * @param classifiedBean the classified to update (gwt bean)
	 * @param token the associated token
	 * @return true if the classified was updated, false otherwise
	 * @throws AuthenticationException if the token authentication failed
	 */
	public boolean updateClassified(ClassifiedBean classifiedBean, Long token) throws AuthenticationException {
		Long id = classifiedBean.getId();
		Long storedToken = tokens.remove(id);
		if (id > 0) {
			if (storedToken != null && storedToken.equals(token)) {
				PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
				try {
					pm.currentTransaction().begin();
					Classified classified = pm.getObjectById(Classified.class, id);
					if (classified != null) {
						classified.setOffer(classifiedBean.isOffer());
						classified.setTitle(classifiedBean.getTitle());
						classified.setDate(classifiedBean.getDate());
						classified.setBody(classifiedBean.getBody());
						classified.setCategory(classifiedBean.getCategory().getValue());
						classified.setContact(classifiedBean.getContact());
					}
					pm.makePersistent(classified);
					pm.currentTransaction().commit();
				} catch (NucleusObjectNotFoundException e) {
					// object does not exist, should not happen
					pm.currentTransaction().rollback();
					System.err.println("This should not happen");
					e.printStackTrace(System.err);
				} finally {
					pm.close();
				}
			} else {
				throw new AuthenticationException("Authentication failed");
			}
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Remove a classified from the database.
	 * 
	 * @param id id of the classified to remove
	 * @param token the authentication token {@see #checkAccess(Long, String) checkAccess} method
	 * @return true if removal was successful, false otherwise
	 * @throws AuthenticationException if the token authentication failed
	 */
	public boolean removeClassified(Long id, Long token) throws AuthenticationException {
		Long storedToken = tokens.remove(id);
		if (id > 0) {
			if (storedToken != null && storedToken.equals(token)) {
				PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
				try {
					pm.currentTransaction().begin();
					Classified classified = pm.getObjectById(Classified.class, id);
					if (classified != null) {
						pm.deletePersistent(classified);
						pm.currentTransaction().commit();
					}
				} catch (NucleusObjectNotFoundException e) {
					// object does not exist, should not happen
					pm.currentTransaction().rollback();
					System.err.println("This should not happen");
					e.printStackTrace(System.err);
				} finally {
					pm.close();
				}
			} else {
				throw new AuthenticationException("Authentication failed");
			}
		}
		return false;
	}
	
	/**
	 * Retrieve all the classifieds in the database.
	 * 
	 * @return a list of classifieds as gwt beans
	 */
	@SuppressWarnings("unchecked")
	public List<ClassifiedBean> getClassifieds() {
		PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
		String query = "select from " + Classified.class.getName();
		List<Classified> classifieds = (List<Classified>) pm.newQuery(query).execute();
		List<ClassifiedBean> classifiedBeans = new ArrayList<ClassifiedBean>(classifieds.size());
		for (Classified cla: classifieds) {
			classifiedBeans.add(new ClassifiedBean(cla.getId(), cla.isOffer(), cla.getTitle(), cla.getBody(), cla.getDate(), new CategoryBean(cla.getCategory()), cla.getContact()));
		}
		return classifiedBeans;
	}
	
	/**
	 * Store a message for feedback purposes. This is a quick and dirty way of getting feedback from users without too much annoyance.
	 * 
	 * @param message the message to send/store
	 */
	public void sendFeedback(FeedbackMessage message) {
		PersistenceManager pm = getPersistenceManagerFactory().getPersistenceManager();
		pm.currentTransaction().begin();
		try {
			pm.makePersistent(message);
			pm.currentTransaction().commit();
		} catch (Exception e) {
			pm.currentTransaction().rollback();
		} finally {
			pm.close();
		}
	}
	
}
